﻿/**
 * Swiff.Uploader Class File
 *
 * @licence		MIT Licence
 *
 * @author		Harald Kirschner <http://digitarald.de>
 * @copyright	Authors
 */

package
{
	import flash.events.*;
	import flash.net.FileReference;
	import flash.net.FileFilter;

	import flash.net.URLRequest;
	import flash.net.URLRequestMethod;
	import flash.net.URLVariables;
	
	/**
	 * @author Harald Kirschner <mail [at] digitarald.de>
	 */
	public class File
	{
		static var idStack:uint = 1;
		
		static const STATUS_QUEUED:uint = 0;
		static const STATUS_RUNNING:uint = 1;
		static const STATUS_ERROR:uint = 2;
		static const STATUS_COMPLETE:uint = 3;
		static const STATUS_STOPPED:uint = 4;

		public var id:uint = 0;
		
		public var status:uint = 0;

		private var options:Object = {
			url: null,
			method: null,
			data: null,
			mergeData: null,
			fieldName: null
		}
		
		public var reference:FileReference = null;
		private var parent:Main = null;
		
		public var responseText:String = null;
		public var responseCode:uint = 0;
		public var responseError:String = null;
		
		public var validationError:String = null;
		
		public var addDate:Date = null;
		public var startDate:Date = null;
		public var progressDate:Date = null;
		public var completeDate:Date = null;
		
		public var fileExtension:String = null;
		
		public var bytesLoaded:uint = 0;
		public var timeElapsed:uint = 0;
		public var timeRemaining:uint = 0;
		
		public var progressGraph:Array = new Array();
		public var rate:uint = 0;
		public var rateAvg:uint = 0;
		
		private var lockProgress:uint = 0;
		
		public function File(parent_init:Main, reference_init:FileReference)
		{
			id = File.idStack++;
			
			reference = reference_init;
			parent = parent_init;
			
			reference.addEventListener(Event.OPEN, handleOpen);
			reference.addEventListener(ProgressEvent.PROGRESS, handleProgress);
			reference.addEventListener(DataEvent.UPLOAD_COMPLETE_DATA, handleComplete);
			reference.addEventListener(HTTPStatusEvent.HTTP_STATUS, handleHttpStatus);
			reference.addEventListener(IOErrorEvent.IO_ERROR, handleIoError);
			reference.addEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError);
			
			fileExtension = reference.name.replace(/.+\.([^.]+)$/, '$1').toLowerCase();
			
			addDate = new Date();
		}
		
		public function fireEvent(functionName:String, queue:Boolean = false):void
		{
			var args:Array = [export()];
			if (queue) args.push(parent.queueUpdate());
			
			parent.fireEvent('file' + functionName.charAt(0).toUpperCase() + functionName.substr(1), args);
		}
		
		private function handleOpen(event:Event):void
		{
			progressDate = startDate = new Date();
			
			fireEvent('open');
		}
		
		private function handleProgress(event:ProgressEvent):void
		{
			var now:uint = new Date().getTime();
			
			var allow:Boolean = (!lockProgress || lockProgress < now - 100 || event.bytesLoaded == reference.size)
			
			if (allow) {
				updateProgress((event.bytesLoaded >= 0) ? event.bytesLoaded : 0);
				
				lockProgress = now;
				fireEvent('progress', true);
			} else {
				// parent.verboseLog('Process Skipped', [event.bytesLoaded, now - lockProgress]);
			}
			
		}
		
		private function resetProgress():void
		{
			responseText = responseError = null;
			responseCode = 0;
			
			progressGraph.length = 0;
			startDate = progressDate = completeDate = null;
			timeElapsed = timeRemaining = rate = rateAvg = bytesLoaded = 0;
		}
		
		private function updateProgress(bytes_loaded:uint, complete:Boolean = false)
		{
			var now:Date = new Date();
			var i:uint = 0, length:uint = 0;
			
			rate = rateAvg = 0;
			
			if (bytes_loaded && !complete) {
				
				var bytes_diff:Number = (bytes_loaded - bytesLoaded) / (now.getTime() - progressDate.getTime());
				length = progressGraph.unshift(Math.round(bytes_diff));
				
				bytesLoaded = bytes_loaded;
				
				if (length > 1) {
					
					if (length > parent.options.progressGraphSize) length = parent.options.progressGraphSize;
					
					var mean:Number = 0;
					var variance:Number = 0;
					
					for (i = 0; i < length; i++) {
						mean += progressGraph[i];
						variance += Math.pow(progressGraph[i] - mean, 2);
					}
					mean /= length;
					
					rate = rateAvg = Math.round(mean * 1000);
					
					if (length > 6) {
						var standard_dev:Number = Math.sqrt(variance / length);
						var deviation_range:Number = 2.0;
						
						var filtered_sum:Number = 0;
						var filtered_count:uint = 0;
						
						for (i = 0; i < length; i++) {
							var value:Number = (progressGraph[i] - mean) / standard_dev;
							
							if (value <= deviation_range && value >= -deviation_range) {
								filtered_sum += progressGraph[i];
								filtered_count++;
							}
						}
						
						rateAvg = Math.round(filtered_sum / filtered_count * 1000);

						timeRemaining = (reference.size - bytes_loaded) / rateAvg;
					}
				}
			}
			
			timeElapsed = (now.getTime() - startDate.getTime()) / 1000;
			
			progressDate = now;
		
			parent.rate = parent.bytesLoaded = 0;
			
			for (var i = 0; i < parent.fileList.length; i++) {
				var file:File = parent.fileList[i];
				switch (file.status) {
					case File.STATUS_RUNNING:
						parent.rate += file.rateAvg;
					case File.STATUS_COMPLETE:
						parent.bytesLoaded += file.bytesLoaded;
				}
			}
		}
		
		private function handleComplete(event:DataEvent):void
		{
			complete(event.data);
		}
		
		private function handleIoError(event:IOErrorEvent):void
		{
			complete(event.text , event.type, null);
		}
		
		private function handleSecurityError(event:SecurityErrorEvent):void
		{
			complete(event.text, event.type, null);
		}
		
		private function handleHttpStatus(event:HTTPStatusEvent):void
		{
			if (parent.options.passStatus is Array) {
				var list:Array = parent.options.passStatus;
				if (list.length && list.indexOf(event.status) != -1) {
					complete(null, null, event.status);
					return;
				}
			}
			
			complete(null, event.type, event.status);
		}
		
		private function complete(text:String = null, error:String = null, code:int = 0)
		{
			if (status != File.STATUS_RUNNING) {
				parent.verboseLog('File[' + id + ']::complete wasted!', [].concat(arguments));
				return;
			}
						
			completeDate = new Date();
			
			responseText = text;
			responseCode = code;
			
			if (error) {
				responseError = error;
				status = File.STATUS_ERROR;
			} else {
				status = File.STATUS_COMPLETE;
			}
			
			parent.uploading--;
			
			updateProgress(reference.size, true);
			
			fireEvent('complete', true);
			
			parent.checkQueue(true);
		}
		
		public function setOptions(options_init:Object = null):void
		{
			if (options_init != null) {
				for (var prop:String in options) {
					if (options_init.hasOwnProperty(prop)) options[prop] = options_init[prop];
				}
			}
		}
		
		public function start():Boolean
		{
			if (status == File.STATUS_RUNNING) return false;
			
			var options_parent:Object = parent.options;
			var merged:Object = new Object();
			
			for (var prop:String in options) {
				merged[prop] = (options[prop] != null) ? options[prop] : options_parent[prop];
			}
			
			try {
				var url_request:URLRequest = new URLRequest(merged.url || '');
				
				if (merged.data != null && merged.data != false) {
					if (merged.mergeData && options.data && options_parent.data) {
						if (options.data is String && options_parent.data is String) {
							merged.data = options_parent.data + '&' + options.data;
						} else {
							merged.data = new Object();
							for (var prop:String in options_parent.data) {
								merged[prop] = options_parent.data[prop];
							}
							for (var prop:String in options.data) {
								merged[prop] = options.data[prop];
							}
						}
					}
					
					var data:URLVariables = new URLVariables();
					if (merged.data is String) data.decode(merged.data);
					else for (var key:Object in merged.data) data[key] = merged.data[key];
					url_request.data = data;
				}
			
				url_request.method = URLRequestMethod[(merged.method) ? merged.method.toUpperCase() : 'POST'];
			} catch (e:Error) {
				parent.verboseLog('File[' + id + ']::start - Exception (URLRequest)', String(e));
				return false;
			}
			
			parent.verboseLog('File[' + id + ']::start', merged);
			
			resetProgress();
			
			status = File.STATUS_RUNNING;
			parent.uploading++;
			
			fireEvent('start', true);
			
			try {
				reference.upload(url_request, merged.fieldName || 'Filedata');
			} catch (e:Error) {
				parent.verboseLog('File[' + id + ']::start - Exception (upload)', String(e));
				stop();
				return false;
			}
			
			return true;
		}
		
		public function stop(eventful:Boolean = true):Boolean
		{
			if (status != File.STATUS_RUNNING) return false;
			
			reference.cancel();
			status = File.STATUS_STOPPED;
			
			parent.uploading--;
			parent.bytesLoaded -= bytesLoaded;
			parent.rate -= rateAvg;
			
			resetProgress();
			
			if (eventful) {
				fireEvent('stop', true);
				parent.checkQueue(true);
			}
			
			return true;
		}
		
		public function requeue():Boolean
		{
			var running:Boolean = stop(false);
			
			status = File.STATUS_QUEUED;
			
			fireEvent('requeue');
			
			if (running) parent.checkQueue(true);
			
			return true;
		}
		
		public function remove():Boolean
		{
			var running:Boolean = stop(false);
			
			var idx = parent.fileList.indexOf(this);
			parent.fileList.splice(idx, 1);

			parent.size -= reference.size;
				
			fireEvent('remove', true);
			
			if (running) parent.checkQueue(true);
			
			reference = null;
			
			return true;
		}
		
		public function validate():Boolean
		{
			if (!parent.options.allowDuplicates && parent.hasFile(this)) {
				validationError = 'duplicate';
				return false;
			}

			if (parent.options.fileSizeMin > 0 && reference.size < parent.options.fileSizeMin) {
				validationError = 'sizeLimitMin';
				return false;
			}
			
			if (parent.options.fileSizeMax > 0 && reference.size > parent.options.fileSizeMax) {
				validationError = 'sizeLimitMax';
				return false;
			}
			
			return true;
		}
		
		public function export():Object
		{
			var export:Object = {
				id: id,
				name: reference.name,
				size: reference.size,
				modificationDate: reference.modificationDate,
				creationDate: reference.creationDate,
				extension: fileExtension,
				status: status,
				validationError: validationError,
				addDate: addDate
			};
			
			if (startDate) {
				export.startDate = startDate;
				export.progressDate = progressDate;
				
				export.progress = {
					graph: progressGraph,
					bytesLoaded: bytesLoaded,
					percentLoaded: Math.ceil(bytesLoaded / reference.size * 100),
					rate: rate,
					rateAvg: rateAvg,
					timeElapsed: timeElapsed,
					timeRemaining: timeRemaining
				};
			};
			
			if (completeDate) {
				export.completeDate = completeDate;
				
				export.response = {
					text: responseText,
					code: responseCode,
					error: responseError
				}
			}
			
			return export;
		}
		
		// Static methods
		
		static public function exportMany(files:Array):Array
		{
			if (!files.length) return null;
			
			return files.map(function(current:File, i:uint, self:Array) {
				return current.export();
			});
		}

	}
	
}